package net.rvb1.gwt.grad.ui.client.cellview.grid;

import com.google.gwt.core.shared.GWT;
import com.google.gwt.dom.client.BrowserEvents;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.resources.client.ImageResource;
import com.google.gwt.resources.client.ImageResource.ImageOptions;
import com.google.gwt.resources.client.ImageResource.RepeatStyle;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.safehtml.shared.SafeHtmlUtils;
import com.google.gwt.user.cellview.client.Column;
import com.google.gwt.user.cellview.client.Header;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.PopupPanel;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.view.client.CellPreviewEvent;
import com.google.gwt.view.client.ProvidesKey;

import net.rvb1.gwt.grad.ui.client.menu.ContextMenu;
import net.rvb1.gwt.grad.ui.client.menu.HasContextMenuHandlers;
import net.rvb1.gwt.grad.ui.client.menu.events.BeforeShowContextMenuEvent;
import net.rvb1.gwt.grad.ui.client.selection.SelectionHelper;

import fr.mikrosimage.gwt.client.ResizableHeader;

/**
 * Specific Google {@link com.google.gwt.user.cellview.client.DataGrid} with special features from
 * the gRad library.<BR/>
 * 
 * <P>
 * The following functionnalities have been added to the default Google
 * {@link com.google.gwt.user.cellview.client.DataGrid}
 * <UL>
 * <LI>a contextual menu : see {@link #setContextMenu(ContextMenu)} and
 * {@link #addBeforeShowContextMenuHandler(BeforeShowContextMenuEvent.Handler)}</LI>
 * <LI>column grouping (interactively or programmatically ) : only one active group is permitted by
 * now (feature not implemented for now)</LI>
 * <LI>filter criterias directly in the grid interface (feature not implemented for now).</LI>
 * <LI>* resizable and draggable column headers (thanks to the <a
 * href="https://github.com/gchatelet/GwtResizableDraggableColumns"> gwt-resizable-draggable-columns
 * library</a>).</LI>
 * </UL>
 * <UL>
 * </UL>
 * </P>
 * 
 * <P>
 * EXPERIMENTAL: NE PAS VOUS BASER SUR LE DOM GENERE PAR CE WIDGET ... La structure des objets DOMs
 * générés par ce widget pourrait changer au fur-et-à-mesure des versions. Ne vous basez donc pas
 * dessus pour vos dév., utilisez plutôt les méthodes de cette classe pour parvenir à vos objectifs.
 * </P>
 * <P>
 * NOTE TO MAINTAINERS : each time GWT is updated in gRad, please verify that
 * {@link #insertColumn(int, Column, Header, Header)} is still the very method called by all other
 * addColumn(..) and insertColumn(..) methods in
 * {@link com.google.gwt.user.cellview.client.DataGrid} (TODO: write some unit tests ...)
 * </P>
 * 
 * 
 * @param <T> Le type de donnée des lignes de cette grille
 * 
 * @see com.google.gwt.user.cellview.client.DataGrid
 */
public class DataGrid<T> extends com.google.gwt.user.cellview.client.DataGrid<T> implements
    HasContextMenuHandlers {

  /**
   * Resizable column headers (thanks to the GwtResizableDraggableColumns library).
   */
  public class DataGridResizableHeader extends ResizableHeader<T> {
    public DataGridResizableHeader(String title, Column<T, ?> column) {
      super(title, DataGrid.this, column);
    }

    @Override
    protected int getTableBodyHeight() {
      return DataGrid.this.getTableBodyElement().getOffsetHeight();
    }
  }

  /**
   * {@link com.google.gwt.user.cellview.client.DataGrid.Resources} specifically tailored to style a
   * gRad {@link DataGrid}.
   */
  public interface Resources extends com.google.gwt.user.cellview.client.DataGrid.Resources {

    @Source("ColumnHeaderBackground.png")
    @ImageOptions(repeatStyle = RepeatStyle.Horizontal)
    ImageResource columnHeaderBackground();

    /**
     * {@inheritDoc}
     * <P>
     * The default Google {@link com.google.gwt.user.cellview.client.DataGrid} CSS is overrided
     * here.
     * </P>
     */
    @Override
    @Source("DataGrid.css")
    Style dataGridStyle();

    /**
     * This image resource is used to style group headers with a specific indicator when the group
     * is effectively collapsed.
     */
    @Source("groupHeaderCollapsed.png")
    ImageResource groupHeaderCollapsed();

    /**
     * This image resource is used to style group headers with a specific indicator when the group
     * is effectively collapsed.
     */
    @Source("groupHeaderExpanded.png")
    ImageResource groupHeaderExpanded();
  }

  /**
   * {@link com.google.gwt.user.cellview.client.DataGrid.Style} specifically tailored to style a
   * gRad {@link DataGrid}.
   */
  public interface Style extends com.google.gwt.user.cellview.client.DataGrid.Style {
    /**
     * CSS style applied to the entire TR html element representing a group header.
     */
    public String groupHeader();
  }

  /**
   * {@link CellPreviewEvent.Handler} used to handle to show this grid's contextual menu.
   * 
   * <P>
   * See {@link DataGrid#setContextMenu(ContextMenu)} for more explanations on how the context menu
   * has to be handled by gRad.
   * </P>
   * 
   * @param <U> Data type displayed by this {@link DataGrid}
   */
  private final class ContextMenuCellPreviewHandler<U> implements CellPreviewEvent.Handler<U> {

    @Override
    public void onCellPreview(final CellPreviewEvent<U> event) {

      // Gestion du menu contextuel de la grille si la cellule n'est pas en mode édition
      // Si l'évt courant est 'contextmenu' est si personne n'annule la BeforeShowContextMenuEvent
      // qu'on lance => on
      // continue.
      // Attn: BeforeShowContextMenuEvent.fire(..) peut renvoyer NULL si
      // BeforeShowContextMenuEvent.getType() n'est pas
      // appelé avant. Si on enlève le test getHandlerCount(..) => rajouter un test pour vérifier
      // que l'event retournée
      // n'est pas NULL
      final NativeEvent nativeEvent = event.getNativeEvent();
      if (!event.isCellEditing()
          && nativeEvent.getType().equals(BrowserEvents.CONTEXTMENU)
          && (getHandlerCount(BeforeShowContextMenuEvent.getType()) == 0 || BeforeShowContextMenuEvent
              .fire(DataGrid.this, event.getContext()).isCanceled() == false)) {

        // Cas particulier : l'utilisateur a sélectionné du texte dans la grille pour faire un
        // copier-coller, dans Word
        // par exemple. Si le texte sélectionné "contient" la cellule ou l'inverse alors nous
        // laissons le navigateur
        // afficher le menu contextuel par défaut.
        if (SelectionHelper.isEventTargetRelatedToSelection(event)) {
          return;
        }

        // On annule l'évènement Cell et on empêche le traitement par défaut de
        // l'evt (sinon affichage du menu contextuel par défaut du navigateur).
        event.setCanceled(true);
        nativeEvent.preventDefault();

        // Affichage du menu contextuel de la grille
        DataGrid.this.contextMenu.selectItem(null);
        DataGrid.this.contextMenu.setPopupPositionAndShow(new PopupPanel.PositionCallback() {
          @Override
          public void setPosition(int offsetWidth, int offsetHeight) {
            DataGrid.this.contextMenu.setPopupPosition(
                (nativeEvent.getClientX() + offsetWidth > Window.getClientWidth()) ? Window
                    .getClientWidth()
                    - offsetWidth : nativeEvent.getClientX(), (nativeEvent.getClientY()
                    + offsetHeight > Window.getClientHeight()) ? Window.getClientHeight()
                    - offsetHeight : nativeEvent.getClientY());
          }
        });
      }
    }

  }

  /** Menu contextuel de cette grille */
  private ContextMenu contextMenu;

  /**
   * Retour de {@link #addCellPreviewHandler(com.google.gwt.view.client.CellPreviewEvent.Handler)}
   * pour le menu contextuel de cette grille
   */
  private HandlerRegistration contextMenuHandlerReg;

  private Resources resources;

  /**
   * @see com.google.gwt.user.cellview.client.DataGrid#DataGrid()
   */
  public DataGrid() {
    this(50);
  }

  /**
   * @see com.google.gwt.user.cellview.client.DataGrid#DataGrid(int)
   */
  public DataGrid(int pageSize) {
    this(pageSize, GWT.<Resources> create(Resources.class));
  }

  /**
   * @see com.google.gwt.user.cellview.client.DataGrid#DataGrid(int, ProvidesKey)
   */
  public DataGrid(int pageSize, ProvidesKey<T> keyProvider) {
    this(pageSize, GWT.<Resources> create(Resources.class), keyProvider);
  }

  /**
   * @see com.google.gwt.user.cellview.client.DataGrid#DataGrid(int,
   *      com.google.gwt.user.cellview.client.DataGrid.Resources)
   */
  public DataGrid(int pageSize, Resources resources) {
    super(pageSize, resources);
    init(resources);
  }

  /**
   * @see com.google.gwt.user.cellview.client.DataGrid#DataGrid(int,
   *      com.google.gwt.user.cellview.client.DataGrid.Resources, ProvidesKey)
   */
  public DataGrid(int pageSize, Resources resources, ProvidesKey<T> keyProvider) {
    super(pageSize, resources, keyProvider);
    init(resources);
  }

  /**
   * @see com.google.gwt.user.cellview.client.DataGrid#DataGrid(int,
   *      com.google.gwt.user.cellview.client.DataGrid.Resources, ProvidesKey, Widget)
   */
  public DataGrid(int pageSize, Resources resources, ProvidesKey<T> keyProvider,
      Widget loadingIndicator) {
    super(pageSize, resources, keyProvider, loadingIndicator);
    init(resources);
  }

  /**
   * @see com.google.gwt.user.cellview.client.DataGrid#DataGrid(ProvidesKey)
   */
  public DataGrid(ProvidesKey<T> keyProvider) {
    this(50, keyProvider);
  }

  @Override
  public HandlerRegistration addBeforeShowContextMenuHandler(
      BeforeShowContextMenuEvent.Handler handler) {
    return addHandler(handler, BeforeShowContextMenuEvent.getType());
  }

  /**
   * Returns the {@link Resources} specific to this gRad {@link DataGrid}.
   */
  public Resources getSpecificResources() {
    return this.resources;
  }

  /**
   * {@inheritDoc}
   * 
   * <P>
   * Remarque: cette méthode est surchargée car dans GWT, elle est appelée par toutes les méthodes
   * addColumn(..) et insertColumn(..)
   * </P>
   */
  @Override
  public void insertColumn(int beforeIndex, Column<T, ?> col, Header<?> header, Header<?> footer) {
    if (header == null || header instanceof DataGrid.DataGridResizableHeader) {
      super.insertColumn(beforeIndex, col, header, footer);
    } else {
      SafeHtml headerValue = null;

      if (header.getValue() instanceof String) {
        headerValue = SafeHtmlUtils.fromTrustedString((String) header.getValue());
      } else if (header.getValue() instanceof SafeHtml) {
        headerValue = (SafeHtml) header.getValue();
      }

      super.insertColumn(beforeIndex, col,
          new DataGridResizableHeader(headerValue.asString(), col), footer);
    }
  }

  /**
   * {@inheritDoc}
   * 
   * <P>
   * Automatically hides this grid's context menu if it has already been set.
   * </P>
   * 
   * @see #setContextMenu(ContextMenu)
   */
  @Override
  public void onResize() {
    super.onResize();

    // En cas de resize, on cache le menu contextuel ...
    // On pourrait essayer de faire en sorte de déplacer le menu aussi mais quelle serait l'ancre
    // sur laquelle se baser ?
    // (idée: l'élement de la Cell)
    // Et que faire quand cette ancre n'est pas affichée à l'écran mais bien dans le DOM ?
    // Ex: la cellule est "cachée" dans le scroll horizontal ...
    if (this.contextMenu != null) {
      this.contextMenu.hide(true);
    }
  }

  /**
   * {@inheritDoc}
   * <P>
   * The context menu will also not be shown if the :<BR/>
   * <UL>
   * <LI>the user edits a cell and right-clicks on it, or</LI>
   * <LI>a {@link BeforeShowContextMenuEvent.Handler} cancels the {@link BeforeShowContextMenuEvent}
   * which is launched before displaying the context menu.
   * </UL>
   * </P>
   * <P>
   * The context menu will be automatically hidden if the parent widget of this {@link DataGrid}
   * calls it's {@link #onResize()} method. For example, this happens when the browser window is
   * resized and you entirely use GWT layout panels in your widget hierarchy.
   * </P>
   * 
   * @see #addBeforeShowContextMenuHandler(net.rvb1.gwt.grad.ui.client.menu.events.BeforeShowContextMenuEvent.Handler)
   */
  @Override
  public void setContextMenu(final ContextMenu menuBar) {
    if (menuBar != null) {

      if (this.contextMenuHandlerReg == null) {
        // sink ONCONTEXTMENU sur toute la datagrid
        // TODO: ne faire un sink que sur les cellules mais ça doit être TRES chaud !!
        // hack: de toute façon, nous utilisons un CellPreviewHandler => les évènements traités
        // ne seront que pour les cellules de la grille !!
        sinkEvents(Event.ONCONTEXTMENU);

        this.contextMenuHandlerReg = addCellPreviewHandler(new ContextMenuCellPreviewHandler<T>());
      }

    } else if (this.contextMenuHandlerReg != null) {

      unsinkEvents(Event.ONCONTEXTMENU);
      this.contextMenuHandlerReg.removeHandler();
      this.contextMenuHandlerReg = null;

    }

    this.contextMenu = menuBar;
  }

  /**
   * Initializes this widget with it's specific CSS class names, tabe builder, etc. ...
   */
  protected void init(Resources resources) {
    setStyleName(resources.dataGridStyle().dataGridWidget());
    setHeaderBuilder(new CustomHeaderBuilder<T>(this, false));
    this.resources = resources;
  }
}
